S3 バケット作成直後に署名付き URL を発行したら SignatureDoesNotMatch になるが、時間の経過で解決する事象について

S3 バケット作成直後に署名付き URL を発行したら SignatureDoesNotMatch になるが、時間の経過で解決する事象について

Clock Icon2024.12.27

こんにちは!AWS 事業本部コンサルティング部のたかくに(@takakuni_)です。

タイトルが長いですが、先日 S3 バケットを作成し署名付き URL を発行することがありました。その際にタイトルの事象に遭遇しまして 2 日詰まりました。

そこで今回はなぜ発生したのかまとめてみたいと思います。

S3 バケットの DNS ルーティングについて

S3 バケットの DNS ルーティングについておさらいします。

image.png

対象の S3 バケットを操作するために、エンドポイント(johnsmith.s3.amazonaws.com)の名前解決を行います。(1)

DNS サーバーは対象エンドポイントをホストしている施設(今回だと施設 B)の IP を応答します。(2)

クライアントは対象 IP アドレスに対してリクエスト送り、S3 の操作を行います。(3,4)

ここまではいつもの DNS のやり取りかと思います。

バージニアリージョン以外の S3 バケットを作成した直後の API リクエストについて

ここからが本題です。S3 バケット johnsmith が東京リージョンで作成されたと仮定します。

image.png

バージニアリージョン以外の S3 バケットのエンドポイント(johnsmith.s3.amazonaws.com)は作成後、 DNS 伝播に時間を要します。(S3 バケット johnsmith が東京リージョンにいることを DNS サーバーに伝播するのに時間を要します。)

そのため、クライアントから johnsmith.s3.amazonaws.com にアクセスした際に DNS 伝播が完了していない場合は DNS サーバーはデフォルトエンドポイント(s3.amazonaws.com)を応答します。(2)

クライアントはデフォルトエンドポイントにアクセスしますが、デフォルトエンドポイントは東京リージョンのエンドポイント(s3.ap-northeast-1.amazonaws.com)へのリダイレクトを返答します。(3,4)

クライアントはようやく S3 のやり取りを東京リージョンのエンドポイントとやり取りできます。(5,6)

この辺りの制御は、以下のドキュメントに記載されているため、興味のある方は是非ご覧ください。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/UsingRouting.html#TemporaryRedirection

私が発生した事象

DNS 伝播が済んでいない状態でのデフォルトエンドポイントのリダイレクトが原因でした。

リダイレクトが生じたことで、署名付き URL のホスト名(johnsmith.s3.amazonaws.com)とリダイレクト先のホスト名(johnsmith.s3.ap-northeast-1.amazonaws.com)が異なり、 SignatureDoesNotMatch のエラーが発生していました。

SignatureDoesNotMatch の一般的なトラブルシューティングには以下のドキュメントが参考になりますが、今回のような時間経過で解決するパターンははじめてで非常に勉強になりました。

https://repost.aws/ja/knowledge-center/s3-presigned-url-signature-mismatch

https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-troubleshooting.html

再現してみる

実際に事象を再現するのが、イメージつきやすいと思うので、サンプルコードを用意してみました。

https://github.com/takakuni-classmethod/blog-sample-code/tree/main/cause-dns-SignatureDoesNotMatch

中身はシンプルで S3 のイベント通知をトリガーに URL を生成、CloudWatch Logs に記録するといった構成です。

Untitled(97).png

Lambda のコードは以下になります。

lambda_function.py
import os
import json
import logging
import urllib.parse
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError

logger = logging.getLogger()
region = os.environ['AWS_REGION']

s3 = boto3.client(
    's3',
    region_name=region,
    config=Config(signature_version='s3v4')
)

def lambda_handler(event, context):
    try:
        bucket = event['Records'][0]['s3']['bucket']['name']
        key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
        url = s3.generate_presigned_url(
            ClientMethod = 'get_object',
            Params = {'Bucket' : bucket, 'Key' : key},
            ExpiresIn = 3600,
            HttpMethod = 'GET'
        )
        logger.info("Got presigned URL: %s", url)
    except ClientError:
        logger.exception(
            "Couldn't get a presigned URL."
        )
        raise
    return url

生成された URL をみてみます。s3.amazonaws.com から始まっていますね。

https://presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com/input/[FILE_NAME].html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED_CREDENTIAL]%2F[DATE]%2F[REGION]%2Fs3%2Faws4_request&X-Amz-Date=[DATE]T[TIME]Z&X-Amz-Expires=[EXPIRATION]&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED_TOKEN]&X-Amz-Signature=[MASKED_SIGNATURE]

curl でアクセスしてみましょう。

s3.amazonaws.com から s3-ap-northeast-1.amazonaws.com へのリダイレクト(307)が発生したのちに、403 エラーが発生しています。

takakuni@ cause-dns-SignatureDoesNotMatch % curl -o takakuni.html "https://presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com/input/takakuni.html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED-CREDENTIAL]&X-Amz-Date=20241227T135505Z&X-Amz-Expires=3600&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED-TOKEN]&X-Amz-Signature=[MASKED-SIGNATURE]" -v -L
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
* Host presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com:443 was resolved.
* IPv6: (none)
* IPv4: [MASKED-IPS]
*   Trying [MASKED-IP]:443...
* Connected to presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com ([MASKED-IP]) port 443
* ALPN: curl offers h2,http/1.1
[TLS handshake details omitted]
* SSL connection using TLSv1.3 / AEAD-AES128-GCM-SHA256
* ALPN: server accepted http/1.1
* Server certificate:
*  subject: CN=*.s3.amazonaws.com
*  start date: Apr 22 00:00:00 2024 GMT
*  expire date: Apr  7 23:59:59 2025 GMT
*  subjectAltName: host "presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com" matched cert's "*.s3.amazonaws.com"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M01
*  SSL certificate verify ok.
* using HTTP/1.x
> GET /input/takakuni.html?[PRESIGNED-URL-PARAMS] HTTP/1.1
> Host: presigned-lambda-[ACCOUNT_ID].s3.amazonaws.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 307 Temporary Redirect
< x-amz-bucket-region: ap-northeast-1
< x-amz-request-id: [MASKED-REQUEST-ID]
< x-amz-id-2: [MASKED-ID]
< Location: https://presigned-lambda-[ACCOUNT_ID].s3-ap-northeast-1.amazonaws.com/[MASKED-REDIRECT-URL]
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Fri, 27 Dec 2024 14:01:55 GMT
< Server: AmazonS3

[Second request details with similar masking...]

< HTTP/1.1 403 Forbidden
< x-amz-request-id: [MASKED-REQUEST-ID]
< x-amz-id-2: [MASKED-ID]
< Content-Type: application/xml
< Transfer-Encoding: chunked
< Date: Fri, 27 Dec 2024 14:01:55 GMT
< Server: AmazonS3

取得した HTML ファイルも SignatureDoesNotMatch と記載されていますね。

<?xml version="1.0" encoding="UTF-8"?>
<Error>
	<code>SignatureDoesNotMatch</code>
	<Message
		>The request signature we calculated does not match the signature you
		provided. Check your key and signing method.</Message
	>
	<AWSAccessKeyId>[MASKED-ACCESS-KEY]</AWSAccessKeyId>
	<StringToSign
		>AWS4-HMAC-SHA256 20241227T135505Z 20241227/ap-northeast-1/s3/aws4_request
		[MASKED-STRING-TO-SIGN-HASH]</StringToSign
	>
	<SignatureProvided>[MASKED-SIGNATURE]</SignatureProvided>
	<StringToSignBytes>[MASKED-BYTES]</StringToSignBytes>
	<CanonicalRequest
		>GET /input/takakuni.html [MASKED-CANONICAL-REQUEST-PARAMS]
		host:presigned-lambda-[ACCOUNT_ID].s3-ap-northeast-1.amazonaws.com host
		UNSIGNED-PAYLOAD</CanonicalRequest
	>
	<CanonicalRequestBytes
		>[MASKED-CANONICAL-REQUEST-BYTES]</CanonicalRequestBytes
	>
	<RequestId>[MASKED-REQUEST-ID]</RequestId>
	<HostId>[MASKED-HOST-ID]</HostId>
</Error>

リージョナルエンドポイントで発行する

上記の通り DNS 伝播が済めば(時間が経てば)、リダイレクトされることなく到達し解決するのですが、恒久的に解決したくなる方もいるのではないでしょうか。その場合、署名付き URL の発行形式(addressing_style)を virtual に変更することで解決ができる可能性があります。

lambda_function.py
import os
import json
import logging
import urllib.parse
import boto3
from botocore.client import Config
from botocore.exceptions import ClientError

logger = logging.getLogger()
region = os.environ['AWS_REGION']

s3 = boto3.client(
    's3',
    region_name=region,
-    config=Config(signature_version='s3v4')
+    config=Config(signature_version='s3v4', s3={'addressing_style': 'virtual'})
)

def lambda_handler(event, context):
    try:
        bucket = event['Records'][0]['s3']['bucket']['name']
        key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
        url = s3.generate_presigned_url(
            ClientMethod = 'get_object',
            Params = {'Bucket' : bucket, 'Key' : key},
            ExpiresIn = 3600,
            HttpMethod = 'GET'
        )
        logger.info("Got presigned URL: %s", url)
    except ClientError:
        logger.exception(
            "Couldn't get a presigned URL."
        )
        raise
    return url

上記の処理を行うことで署名付き URL が s3.amazonaws.com ではなく s3.ap-northeast-1.amazonaws.com で発行されます。

https://presigned-lambda-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com/input/[FILE_NAME].html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED_CREDENTIAL]%2F[DATE]%2F[REGION]%2Fs3%2Faws4_request&X-Amz-Date=[DATE]T[TIME]Z&X-Amz-Expires=[EXPIRATION]&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED_TOKEN]&X-Amz-Signature=[MASKED_SIGNATURE]

同じく curl の実行結果です。こちらは 200 OK となりました。

curl -o [FILE_NAME].html "https://presigned-lambda2-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com/input/[FILE_NAME].html?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=[MASKED_CREDENTIAL]%2F[DATE]%2F[REGION]%2Fs3%2Faws4_request&X-Amz-Date=[DATE]T[TIME]Z&X-Amz-Expires=[EXPIRATION]&X-Amz-SignedHeaders=host&X-Amz-Security-Token=[MASKED_TOKEN]&X-Amz-Signature=[MASKED_SIGNATURE]" -v -L

* Host presigned-lambda2-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com:443 was resolved.
* IPv4: [MASKED_IPS]
* Connected to presigned-lambda2-[ACCOUNT_ID].s3.ap-northeast-1.amazonaws.com ([MASKED_IP]) port 443
[SSL/TLS connection details masked]
* Server certificate:
*  subject: CN=*.s3-ap-northeast-1.amazonaws.com
*  start date: [MASKED_DATE]
*  expire date: [MASKED_DATE]
*  subjectAltName: host "[MASKED_HOST]" matched cert's "[MASKED_CERT]"
*  issuer: C=US; O=Amazon; CN=Amazon RSA 2048 M01

< HTTP/1.1 200 OK
< x-amz-id-2: [MASKED_ID]
< x-amz-request-id: [MASKED_REQUEST_ID]
< Date: [MASKED_DATE]
< Last-Modified: [MASKED_DATE]
< x-amz-expiration: expiry-date="[MASKED_DATE]", rule-id="[RULE_ID]"
< ETag: [MASKED_ETAG]
< x-amz-server-side-encryption: AES256
< Content-Type: application/octet-stream
< Content-Length: [LENGTH]
< Server: AmazonS3
{ [64 bytes data]
100    64  100    64    0     0    423      0 --:--:-- --:--:-- --:--:--   426
* Connection #0 to host presigned-lambda2-622809842341.s3.ap-northeast-1.amazonaws.com left intact

addressing_style が virtual でも通用しないケース

addressing_style が virtual でも通用しないケースとして、ピリオドを含むバケット名が挙げられます。

そもそも、仮想ホスト形式ではピリオドを含むバケット名がサポートされていないため、署名付き URL とは別の話だと思いますが念の為。

https://dev.classmethod.jp/articles/s3-no-longer-support-path-style-requests/

まとめ

以上、「S3 バケット作成直後に署名付き URL を発行したら SignatureDoesNotMatch になるが、時間の経過で解決する事象について」でした。

私はこの事象を解決するまでに 2 日もかかってしまいました。このブログがどなたかの参考になれば幸いです。

AWS 事業本部コンサルティング部のたかくに(@takakuni_)でした!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.